﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Speech.Recognition;
using System.Runtime.InteropServices;
using System.Xml;

namespace WowVoiceBox
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        System.Speech.Recognition.SpeechRecognizer sr;
        DictationGrammar general;
        Grammar chatGrammar;
        Grammar gameCommands;
        Grammar appCommands;
        private enum commandModes {Commands, Dictation};
        private SpeechCommand curCommand;
        commandModes mode;

        private Dictionary<String,SpeechCommand> commands;
        
        [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
        [DllImport("user32")]
        static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int FindWindow(string lpClassName, string lpWindowName);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int FindWindowEx(int hwndParent,
                            int hwndChildAfter,
                            string lpszClass,
                            string lpszWindow);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int SendMessage(int hwnd,
                            int wMsg,
                            int wParam,
                            int lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int SendNotifyMessage(int hwnd,
                            int wMsg,
                            int wParam,
                            int lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int SendMessage(int hwnd,
                            int wMsg,
                            int wParam,
                            string lParam);

        //[DllImport("User32.dll", SetLastError = true)]
        //public static extern int SendInput(int nInputs, ref INPUT pInputs, int cbSize);


        /// <summary>
        /// The GetForegroundWindow function returns a handle to the foreground window.
        /// </summary>
        [DllImport("user32.dll")]
        static extern int GetForegroundWindow();

        public Window1()
        {
            InitializeComponent();                      
        }

        public void Initialize()
        {
            LogFile.InitLogs(true);
            LogFile.PostMessage("Initializing program");

            commands = new Dictionary<string, SpeechCommand>();

            try   // to load the game commands from the commands.xml file 
            {
               // XmlDocument SettingsFile = new XmlDocument();
                //SettingsFile.PreserveWhitespace = true;
                LogFile.PostMessage("Loading commands file");
                //SettingsFile.Load("commands.xml");

                XmlNodeList commandNodes = WowVoiceBox.Properties.Settings.Default.CommandsXml.SelectNodes("/commands/command");//SettingsFile.SelectNodes("/commands/command");

                foreach (XmlNode node in commandNodes)
                {
                    SpeechCommand newCmd = new SpeechCommand(node);
                    commands.Add(newCmd.VoiceCommand, newCmd);
                }
                LogFile.PostMessage("Commands file loaded");

            }
            catch (Exception ex)
            {
                LogFile.PostMessage("Error loading configuration: " + ex.Message, LogFile.LogMsgType.ErrorMsg);
                MessageBox.Show("Error loading configuration.\n\n" + ex.Message + "\n\nPlease correct the error and try again.", 
                                "Configuration Error", MessageBoxButton.OK, MessageBoxImage.Error);
                this.Close();
                return;
            }

            try
            {
                LogFile.PostMessage("Initializing speech recognition");

                sr = new SpeechRecognizer();

                BuildGameCommandsGrammar();

                LogFile.PostMessage("Building dictation commands grammar");
                // Set up dictation voice commands grammar
                Choices chatCommands = new Choices();
                chatCommands.Add("Send");
                chatCommands.Add("Clear");
                chatCommands.Add("Cancel");
                GrammarBuilder chatGB = new GrammarBuilder(chatCommands);
                chatGB.Culture = sr.RecognizerInfo.Culture; // Have to set the culture or non US english version have a problem
                chatGrammar = new Grammar(chatGB);

                LogFile.PostMessage("Building application commands grammar");
                // Set up application voice commands grammar
                Choices appChoices = new Choices();
                appChoices.Add("Quit");
                appChoices.Add("Options");
                GrammarBuilder appGB = new GrammarBuilder(appChoices);
                appGB.Culture = sr.RecognizerInfo.Culture; // Have to set the culture or non US english version have a problem
                appCommands = new Grammar(appGB);

                LogFile.PostMessage("Creating dictation grammar");
                // Set up dictation grammar
                general = new DictationGrammar();

                LogFile.PostMessage("Loading application commands grammar");
                // Start with the game and app commands grammars loaded
                sr.LoadGrammar(appCommands);

                mode = commandModes.Commands;

                LogFile.PostMessage("Registering speech recognition events");
                sr.SpeechRecognized += new EventHandler<SpeechRecognizedEventArgs>(sr_SpeechRecognized);
                sr.SpeechHypothesized += new EventHandler<SpeechHypothesizedEventArgs>(sr_SpeechHypothesized);

                LogFile.PostMessage("Speech recognition initialization completed");
            }
            catch (Exception ex)
            {
                LogFile.PostMessage("Error initializing speech recognition: " + ex.Message, LogFile.LogMsgType.ErrorMsg);
                MessageBox.Show("Error initializing speech recognition.\n\n" + ex.Message + "\n\nPlease correct the error and try again.",
                                "Speech Recognition Error", MessageBoxButton.OK, MessageBoxImage.Error);
                this.Close();
                return;
            }

            lbCorrectionOptions.Visibility = Visibility.Hidden;

            LogFile.PostMessage("Program running...");
        }

        // Displays the options window modally
        public void DisplayOptions()
        {
            OptionsWindow op = new OptionsWindow(commands.Values.ToList());
            if (op.ShowDialog().Value)
            {
                // Rebuild the commands and save them if the user hit OK
                commands = new Dictionary<string, SpeechCommand>();
                foreach (SpeechCommand cmd in op.CommandList)
                {
                    commands.Add(cmd.VoiceCommand, cmd);
                }

                BuildGameCommandsGrammar();
                SaveCommandsFile();
            }
        }

        // Saves the current list of commands to file
        public void SaveCommandsFile()
        {
            XmlDocument SettingsFile = new XmlDocument();

            // Create XML declaration
            XmlDeclaration xmldecl;
            xmldecl = SettingsFile.CreateXmlDeclaration("1.0", null, null);
            xmldecl.Encoding = "UTF-8";
            xmldecl.Standalone = "yes";

            // Add the declaration to the document.
            XmlElement root = SettingsFile.DocumentElement;
            SettingsFile.InsertBefore(xmldecl, root);

            LogFile.PostMessage("Saving commands file...");
            XmlNode commandsNode = SettingsFile.CreateElement("commands");

            foreach (SpeechCommand cmd in commands.Values)
            {
                XmlNode newCmdNode = cmd.ToXmlNode(SettingsFile);
                commandsNode.AppendChild(newCmdNode);
            }
            SettingsFile.AppendChild(commandsNode);

            //SettingsFile.Save(System.Windows.Forms.Application.LocalUserAppDataPath + "\\" + "commands.xml");
            WowVoiceBox.Properties.Settings.Default.CommandsXml = SettingsFile;
            WowVoiceBox.Properties.Settings.Default.Save();

            LogFile.PostMessage("Commands file saved");
        }

        public void BuildGameCommandsGrammar()
        {
            LogFile.PostMessage("Building game commands grammar");

            // Unload current gameCommands if it exists
            if (gameCommands != null && sr.Grammars.Contains(gameCommands))
                sr.UnloadGrammar(gameCommands);

            //Build Choice List
            Choices grammarChoices = new Choices();

            // Add from XML commands
            foreach (String cmd in commands.Keys)
            {
                grammarChoices.Add(cmd);
            }

            //Add to Grammar Builder
            GrammarBuilder gb = new GrammarBuilder(grammarChoices);
            gb.Culture = sr.RecognizerInfo.Culture; // Have to set the culture or non US english version have a problem

            // Create Grammar object from our grammar builder
            gameCommands = new Grammar(gb);

            // Load the new Grammar into the recognizer
            sr.LoadGrammar(gameCommands);
        }

        void sr_SpeechHypothesized(object sender, SpeechHypothesizedEventArgs e)
        {
             //textBox1.Text = e.Result.Text;
        }

        // This event handles when the speech recognition engine has fully recognized a phrase
        void sr_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            // If we are accepting dictation there's several possibilities
            if (mode == commandModes.Dictation)
            {
                if (string.Compare(e.Result.Text, "Send", true) == 0)
                {
                    SendMessage();  // This will pull the dictated text from the TextBox

                    // Put us back into commands mode
                    sr.UnloadGrammar(chatGrammar);
                    sr.UnloadGrammar(general);
                    sr.LoadGrammar(gameCommands);
                    mode = commandModes.Commands;
                    lblCurrentCommand.Content = "";
                    tbDictatedText.Text = "";
                    this.Topmost = false;
                    this.BeginStoryboard(Timeline2_BeginStoryboard1.Storyboard);
                }
                else if (string.Compare(e.Result.Text, "Clear", true) == 0)
                {
                    // Just clears any dictated text
                    tbDictatedText.Text = "";
                }
                else if (string.Compare(e.Result.Text, "Cancel", true) == 0)
                {
                    // Puts us back into commands mode without sending any thing
                    sr.UnloadGrammar(chatGrammar);
                    sr.UnloadGrammar(general);
                    sr.LoadGrammar(gameCommands);
                    mode = commandModes.Commands;
                    lblCurrentCommand.Content = "";
                    tbDictatedText.Text = "";
                    this.Topmost = false;
                    this.BeginStoryboard(Timeline2_BeginStoryboard1.Storyboard);
                }
                else
                {
                    // Didn't recognize any command, so add the dictated text to the TextBox
                    tbDictatedText.Text += e.Result.Text + " ";

                    // TODO: I was trying to add the possibility to correct dictated text,
                    //       but it doesn't work yet.
                    lbCorrectionOptions.ItemsSource = null;
                    lbCorrectionOptions.Items.Clear();
                    List<string> wordList = new List<string>();

                    foreach (RecognizedPhrase rp in e.Result.Alternates)
                    {
                        wordList.Add(rp.Text);
                    }

                    lbCorrectionOptions.ItemsSource = wordList;

                    lbCorrectionOptions.SelectedIndex = -1;
                }
            }
            else  // We were accepting commands
            {
                if (commands.ContainsKey(e.Result.Text))  // See if it was in the list of game commands
                {
                    // Get the command
                    curCommand = commands[e.Result.Text];

                    if (curCommand.AcceptsDictation)
                    {
                        // Go into dictation mode
                        sr.UnloadGrammar(gameCommands);
                        sr.LoadGrammar(chatGrammar);
                        sr.LoadGrammar(general);
                        mode = commandModes.Dictation;
                        this.Topmost = true;
                        lblCurrentCommand.Content = curCommand.VoiceCommand + ":";
                        this.BeginStoryboard(Timeline1_BeginStoryboard.Storyboard);
                    }
                    else
                    {
                        // Just send the current command message
                        SendMessage();
                    }
                }
                else   // It must be an application command
                {
                    switch (e.Result.Text)
                    {
                        case "Options":
                            DisplayOptions();
                            break;
                        case "Quit":
                            this.Close();
                            break;
                    }
                }
            }
        }

        // Handle the quit button
        private void button2_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }

        /* This is the function that sends messages to the client(s).
         * 
         * Currently it does it's own broadcasting, which means the keystrokes
         * will only get broadcast to the local computer and can't be picked up
         * by another broadcaster and sent over the network.
         * 
         * Much of the commented code can be used to raise keyboard events rather
         * than send messages to individual windows, which should make it possible
         * for a broadcaster to pick them up.  However I ran into trouble converting
         * strings to the right keycodes for that function, so I gave up on it for now.
         */
        public void SendMessage()
        {            
            List<int> wowWindows = new List<int>();

            // If it's a broadcast command, we need to get all windows titled "World of Warcraft"
            // TODO: In the future, this may need to be changed to allow other games.
            if (curCommand.Broadcast)
            {
                System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcesses();

                foreach (System.Diagnostics.Process p in processes)
                {
                    if (WowVoiceBox.Properties.Settings.Default.WowWindowNames.Contains(p.MainWindowTitle))
                        wowWindows.Add(p.MainWindowHandle.ToInt32());
                }
            }
            else
            {   // Not a broadcast, so just add the foreground window 
                wowWindows.Add(GetForegroundWindow());
            }

            if (curCommand.CommandType == SpeechCommandType.Macro)
            {
                /*
                if (curCommand.Broadcast)
                {
                    //SendMessage(foreWnd, 0x100, 0x0D, 0);
                    //System.Threading.Thread.Sleep(25);
                    //SendMessage(foreWnd, 0x101, 0x0D, 0);
                    //System.Threading.Thread.Sleep(25);
                    //System.Windows.Forms.SendKeys.SendWait("{ENTER}" + String.Format(curCommand.CommandString, tbDictatedText.Text) + "{ENTER}");
                    
                    keybd_event(0x0D, 0x45, 0, 0);
                    keybd_event(0x0D, 0x45, 0x2, 0);
                    
                    char[] chars = (String.Format(curCommand.CommandString, tbDictatedText.Text)).ToUpper().ToCharArray();
                    //int letterCount = 0;
                    foreach (char c in chars)
                    {
                        int i = Convert.ToInt32(c);
                        
                        switch (i)
                        {
                            case 47:
                                i = 0xBF;
                                break;
                        }
                        
                        //letterCount++;
                        System.Threading.Thread.Sleep(1000);
                        //if (letterCount > 14)
                        //{
                            //SendMessage(foreWnd, 258, i, 0);
                        //    keybd_event((byte)(i), 0x45, 0, 0);
                        //    keybd_event((byte)(i), 0x45, 0x2, 0);
                        //    letterCount = 0;
                        //}
                        //else
                        //{
                            //SendNotifyMessage(foreWnd, 258, i, 0);
                            keybd_event((byte)(i), 0x45, 0, 0);
                            keybd_event((byte)(i), 0x45, 0x2, 0);
                        //}

                        //System.Threading.Thread.Sleep(25);
                        //SendMessage(foreWnd, 0x101, i, 0);
                        //System.Threading.Thread.Sleep(25);
                    }
                    //System.Threading.Thread.Sleep(25);
                    //SendMessage(foreWnd, 0x100, 0x0D, 0);
                    System.Threading.Thread.Sleep(1000);
                    //SendMessage(foreWnd, 0x101, 0x0D, 0);
                    keybd_event(0x0D, 0x45, 0, 0);
                    keybd_event(0x0D, 0x45, 0x2, 0);
                    
                }
                else
                {*/
                foreach (int wndHandle in wowWindows)
                {
                    SendMessage(wndHandle, 0x100, 0x0D, 0);
                    //System.Threading.Thread.Sleep(25);
                    SendMessage(wndHandle, 0x101, 0x0D, 0);
                    //System.Threading.Thread.Sleep(25);

                    char[] chars = (String.Format(curCommand.CommandString, tbDictatedText.Text)).ToCharArray();
                    int letterCount = 0;
                    foreach (char c in chars)
                    {
                        int i = Convert.ToInt32(c);
                        letterCount++;

                        if (letterCount > 14)
                        {
                            SendMessage(wndHandle, 258, i, 0);
                            letterCount = 0;
                        }
                        else
                        {
                            SendNotifyMessage(wndHandle, 258, i, 0);
                        }

                        //System.Threading.Thread.Sleep(25);
                        //SendMessage(foreWnd, 0x101, i, 0);
                        //System.Threading.Thread.Sleep(25);
                    }
                    //System.Threading.Thread.Sleep(25);
                    SendMessage(wndHandle, 0x100, 0x0D, 0);
                    //System.Threading.Thread.Sleep(25);
                    SendMessage(wndHandle, 0x101, 0x0D, 0);
                }
            }
            else
            {
                /*
                // Broadcast mode will simulate keystrokes rather than send messages to a specific window
                // This should allow keyclone to pick it up and broadcast it.
                if (curCommand.Broadcast)
                {
                    NativeWIN32.INPUT curKey = new NativeWIN32.INPUT();
                    curKey.type = 1;
                    // "press" the modifiers
                    foreach (int key in curCommand.KeyModifiers)
                    {
                        //SendMessage(foreWnd, 0x100, key, 0);
                        keybd_event((byte)key, 0x45, 0, 0);
                        //NativeWIN32.INPUT curKey = new NativeWIN32.INPUT();
                        curKey.ki.wVk = (ushort)key;
                        //NativeWIN32.SendInput(1, ref curKey, Marshal.SizeOf(curKey));
                    }

                    // press and release the key
                    //SendMessage(foreWnd, 0x100, curCommand.CommandKey, 0);
                    keybd_event((byte)curCommand.CommandKey, 0x45, 0, 0);
                    curKey.ki.wVk = (ushort)curCommand.CommandKey;
                    //NativeWIN32.SendInput(1, ref curKey, Marshal.SizeOf(curKey));

                    System.Threading.Thread.Sleep(25);
                    //SendMessage(foreWnd, 0x101, curCommand.CommandKey, 0);
                    keybd_event((byte)curCommand.CommandKey, 0x45, 0x2, 0);
                    curKey.ki.dwFlags = 0x0002;
                    //NativeWIN32.SendInput(1, ref curKey, Marshal.SizeOf(curKey));

                    // release the modifiers
                    foreach (int key in curCommand.KeyModifiers)
                    {
                        //SendMessage(foreWnd, 0x101, key, 0);
                        keybd_event((byte)key, 0x45, 0x2, 0);
                        curKey.ki.wVk = (ushort)key;
                        //NativeWIN32.SendInput(1, ref curKey, Marshal.SizeOf(curKey));
                    }
                }
                else
                {*/
                foreach(int wndHandle in wowWindows)
                {
                    // "press" the modifiers
                    foreach (int key in curCommand.KeyModifiers)
                    {
                        SendMessage(wndHandle, 0x100, key, 0);
                    }

                    // press and release the key
                    SendMessage(wndHandle, 0x100, curCommand.CommandKey, 0);
                    System.Threading.Thread.Sleep(25);
                    SendMessage(wndHandle, 0x101, curCommand.CommandKey, 0);

                    // release the modifiers
                    foreach (int key in curCommand.KeyModifiers)
                    {
                        SendMessage(wndHandle, 0x101, key, 0);
                    }
                }
            }
            //System.Threading.Thread.Sleep(25);
            
            /*
            System.Windows.Forms.SendKeys.SendWait("/");
            System.Threading.Thread.Sleep(25);
            System.Windows.Forms.SendKeys.SendWait(curCommand + tbDictatedText.Text);
            //System.Threading.Thread.Sleep(25);
            System.Windows.Forms.SendKeys.SendWait("{ENTER}");
             
            */
        }

        // Just to handle moving hte window
        private void window_MouseDown(object sender, MouseButtonEventArgs e)
        {
            this.DragMove();
        }

        // I was having trouble with the initialization code not working properly if called
        // from the constructor, so I decided to call it when the window is finished loading.
        private void window_Loaded(object sender, RoutedEventArgs e)
        {
            // Most errors should get handled inside Initialize, but added the try-catch
            // block here just in case any sneak through
            try
            {
                Initialize();
            }
            catch (Exception ex)    
            {
                LogFile.PostMessage("Error initializing application: " + ex.Message, LogFile.LogMsgType.ErrorMsg);
                MessageBox.Show("Error initializing application.\n\n" + ex.Message + "\n\nPlease correct the error and try again.",
                                "Initialization Error", MessageBoxButton.OK, MessageBoxImage.Error);
                this.Close();
                return;
            }
        }

        private void window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            LogFile.PostMessage("Application shutting down");
            if (sr != null)
                sr.Dispose();
            LogFile.CloseLogs();
        }

        
    }
}
